package com.tianji.promotion.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.*;
import com.tianji.promotion.constants.PromotionConstants;
import com.tianji.promotion.domain.dto.CouponFormDTO;
import com.tianji.promotion.domain.dto.CouponIssueFormDTO;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.domain.query.CouponQuery;
import com.tianji.promotion.domain.vo.CouponDetailVO;
import com.tianji.promotion.domain.vo.CouponPageVO;
import com.tianji.promotion.domain.vo.CouponScopeVO;
import com.tianji.promotion.domain.vo.CouponVO;
import com.tianji.promotion.enums.CouponStatus;
import com.tianji.promotion.enums.ObtainType;
import com.tianji.promotion.enums.UserCouponStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.ICouponService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.service.IUserCouponService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * <p>
 * 优惠券的规则信息 服务实现类
 * </p>
 *
 * @author 林云
 * @since 2023-08-22
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> implements ICouponService {

    private final ICouponScopeService scopeService;

    private final IExchangeCodeService exchangeCodeService;

    private final IUserCouponService userCouponService;

    private final StringRedisTemplate redisTemplate;

    /**
     * 新增优惠券
     */
    @Override
    @Transactional
    public void saveCoupon(CouponFormDTO dto) {
        //1.dto转vo 保存到coupon表
        Coupon coupon = BeanUtils.copyBean(dto, Coupon.class);
        this.save(coupon);
        //2.判断是否限定使用范围 dto.specific
        //2.1 不限定直接返回
        if (!dto.getSpecific()) {
            return;
        }
        //2.2 限定需要校验scopes
        List<Long> scopes = dto.getScopes();
        if (CollUtils.isEmpty(scopes)) {
            throw new BizIllegalException("优惠券使用范围不能为空");
        }
        //3.批量保存到coupon_scope
        List<CouponScope> couponScopeList = scopes.stream().map(bizId -> new CouponScope()
                .setBizId(bizId)
                .setType(1)
                .setCouponId(coupon.getId())).collect(Collectors.toList());
        scopeService.saveBatch(couponScopeList);
    }

    /**
     * 分页查询优惠券
     */
    @Override
    public PageDTO<CouponPageVO> queryCouponPage(CouponQuery query) {
        //1.分页查询
        Page<Coupon> page = this.lambdaQuery()
                .eq(query.getType() != null, Coupon::getDiscountType, query.getType())
                .eq(query.getStatus() != null, Coupon::getStatus, query.getStatus())
                .like(StringUtils.isNotBlank(query.getName()), Coupon::getName, query.getName())
                .page(query.toMpPageDefaultSortByCreateTimeDesc());
        List<Coupon> records = page.getRecords();
        if (CollUtils.isEmpty(records)) {
            return PageDTO.empty(page);
        }
        //2.封装vo
        List<CouponPageVO> voList = BeanUtils.copyList(records, CouponPageVO.class);
        return PageDTO.of(page, voList);
    }

    /**
     * 修改优惠券
     */
    @Override
    public void updateCoupon(Long id, CouponFormDTO dto) {
        //查询优惠券是否存在
        Coupon couponDB = this.getById(id);
        if (couponDB == null) {
            throw new BizIllegalException("优惠券不存在");
        }
        //查询优惠券是否为待发放状态
        if (!couponDB.getStatus().equals(CouponStatus.DRAFT)) {
            throw new BizIllegalException("只有待发放的优惠券才能更改");
        }
        //转为po
        Coupon coupon = BeanUtils.copyBean(dto, Coupon.class);
        coupon.setId(id);
        //更新
        this.updateById(coupon);
    }

    /**
     * 删除优惠券
     */
    @Override
    @Transactional
    public void deleteCoupon(Long id) {
        //校验
        Coupon couponDB = this.getById(id);
        if (couponDB == null || !couponDB.getStatus().equals(CouponStatus.DRAFT)) {
            throw new BizIllegalException("!couponDB.getStatus().equals(CouponStatus.DRAFT)");
        }
        //删除优惠券
        boolean success = this.remove(new LambdaQueryWrapper<Coupon>().eq(Coupon::getId, id)
                .eq(Coupon::getStatus, CouponStatus.DRAFT));
        if (!success) {
            throw new BadRequestException("优惠券不存在或者优惠券正在使用中");
        }
        //没指定范围就不需要从scope表中移除
        if (!couponDB.getSpecific()) {
            return;
        }
        //删除分类表中的数据
        scopeService.remove(new LambdaQueryWrapper<CouponScope>().eq(CouponScope::getCouponId, id));
    }

    /**
     * 根据id查询优惠券
     */
    @Override
    public CouponDetailVO getCouponById(Long id) {
        //查询优惠券
        Coupon couponDB = this.getById(id);
        //转vo
        CouponDetailVO vo = BeanUtils.copyBean(couponDB, CouponDetailVO.class);
        //查询优惠券是否存在
        if (vo == null || !vo.getSpecific()) {
            return vo;
        }

        //判断是否限定范围
        if (couponDB.getSpecific()) {
            //如果限定范围，需要封装scopes
            List<CouponScope> scopes = scopeService.lambdaQuery().eq(CouponScope::getCouponId, id).list();
            if (CollUtils.isEmpty(scopes)) {
                return vo;
            }
            List<CouponScopeVO> voList = scopes.stream().map(scope -> new CouponScopeVO()
                    .setId(scope.getId()).setName("IT互联网")).collect(Collectors.toList());
            vo.setScopes(voList);
        }
        return vo;
    }

    /**
     * 发放优惠券
     */
    @Override
    public void issueCoupon(Long id, CouponIssueFormDTO dto) {
        log.debug("发放优惠券---线程名---{}", Thread.currentThread().getName());
        //校验参数
        if (id == null || !id.equals(dto.getId())) {
            throw new BadRequestException("非法参数");
        }
        //查询优惠券
        Coupon coupon = this.getById(id);
        if (coupon == null) {
            throw new BadRequestException("优惠券不存在");
        }
        //只只有待发放和暂停中的优惠券才能发放
        if (!coupon.getStatus().equals(CouponStatus.DRAFT) && !coupon.getStatus().equals(CouponStatus.PAUSE)) {
            throw new BizIllegalException("只有待发放和暂停中的优惠券才能发放");
        }
        //判断是否为立刻发放 无起始时间或者起始时间<=当前时间
        LocalDateTime now = LocalDateTime.now();
        boolean isBeginIssue = dto.getIssueBeginTime() == null || !dto.getIssueBeginTime().isAfter(now);
        //使用临时的tmp去做更新，保留之前的数据
        Coupon tmp = BeanUtils.copyBean(dto, Coupon.class);
        if (isBeginIssue) {
            tmp.setStatus(CouponStatus.ISSUING);
            tmp.setIssueBeginTime(now);
        } else {
            tmp.setStatus(CouponStatus.UN_ISSUE);
        }
        this.updateById(tmp);

        //立刻发放，将优惠券id、开始结束时间、发行总数量、限领数量写进Redis中
        if (isBeginIssue) {
            String key = PromotionConstants.COUPON_CACHE_KEY_PREFIX + id;
            redisTemplate.opsForHash().put(key, "issueBeginTime", String.valueOf(DateUtils.toEpochMilli(now)));// 转换为long类型
            redisTemplate.opsForHash().put(key, "issueEndTime", String.valueOf(DateUtils.toEpochMilli(dto.getIssueEndTime())));
            redisTemplate.opsForHash().put(key, "totalNum", String.valueOf(coupon.getTotalNum()));
            redisTemplate.opsForHash().put(key, "userLimit", String.valueOf(coupon.getUserLimit()));
        }

        //如果领取方式为指定发放且待发放，则需要生成兑换码
        if (coupon.getObtainWay().equals(ObtainType.ISSUE) && coupon.getStatus().equals(CouponStatus.DRAFT)) {
            coupon.setIssueEndTime(dto.getIssueEndTime());
            exchangeCodeService.asyncGenerateExchangeCode(coupon);
        }
    }

    /**
     * 查询发放中的优惠券列表
     */
    @Override
    public List<CouponVO> queryIssuingCoupons() {
        //1.查询db coupon表 status为进行中 obtain_way为手动领取
        List<Coupon> couponList = this.lambdaQuery().eq(Coupon::getStatus, CouponStatus.ISSUING)
                .eq(Coupon::getObtainWay, ObtainType.PUBLIC)
                .list();
        if (CollUtils.isEmpty(couponList)) {
            return CollUtils.emptyList();
        }
        //2.po转vo
        //2.1 优惠券id集合 用于查询用户券
        List<Long> couponIds = couponList.stream().map(Coupon::getId).collect(Collectors.toList());
        //2.2 查询用户券 包含在优惠券id集合中
        List<UserCoupon> userCouponList = userCouponService.lambdaQuery().eq(UserCoupon::getUserId, UserContext.getUser())
                .in(UserCoupon::getCouponId, couponIds).list();
        //2.3 统计用户已领取数量 couponId -> num
        Map<Long, Long> issueMap = userCouponList.stream().collect(Collectors.groupingBy(UserCoupon::getCouponId, Collectors.counting()));
        //2.4 统计用户已领取并未使用数量 couponId -> num
        Map<Long, Long> unUseMap = userCouponList.stream()
                .filter(c -> c.getStatus().equals(UserCouponStatus.UNUSED))
                .collect(Collectors.groupingBy(UserCoupon::getCouponId, Collectors.counting()));
        List<CouponVO> voList = new ArrayList<>();
        for (Coupon c : couponList) {
            CouponVO vo = BeanUtils.copyBean(c, CouponVO.class);
            Long issueNum = issueMap.getOrDefault(c.getId(), 0L);//某张券已领取总数量
            boolean isAvailable = c.getIssueNum() < c.getTotalNum() && issueNum.intValue() < c.getUserLimit();
            vo.setAvailable(isAvailable);//是否能领取:有剩余（issue_num<total_num） 用户领取数量小于最大领取数量（用户领取数量<user_limit）
            Long unUseNum = unUseMap.getOrDefault(c.getId(), 0L);//某张券已领取且未使用总数量
            boolean isReceived = unUseNum > 0L;
            vo.setReceived(isReceived);//是否能用:用户领取过至少一张且未使用
            voList.add(vo);
        }
        return voList;
    }
}
